<!-- This file has been written by Me-No-Dev
see https://github.com/me-no-dev/ and is part
of the sample code of ESPAsyncWebServer
see https://github.com/me-no-dev/ESPAsyncWebServer -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>ESP8266 SPIFFS File Editor</title>
  <meta name="author" content="Me-No-Dev" />
  <meta name="Description" content="ESP8266 SPIFFS File Web Editor"/>
  <link rel="shortcut icon" href="favicon.ico"/>
  <style type="text/css" media="screen">
  .cm {z-index:300;position:absolute;left:5px;border:1px solid #444;background-color:#F5F5F5;display:none;box-shadow: 0 0 10px rgba( 0, 0, 0, .4 );font-size:12px;font-family:sans-serif;font-weight:bold;}
  .cm ul {list-style:none;top:0;left:0;margin:0;padding:0;}
  .cm li {position:relative;min-width:60px;cursor:pointer;}
  .cm span {color: #444;display:inline-block;padding:6px;}
  .cm li:hover { background: #444; }
  .cm li:hover span { color: #EEE; }
  .tvu ul, .tvu li {padding:0;margin:0;list-style:none;}
  .tvu input {position:absolute;opacity:0;}
  .tvu {font: normal 12px Verdana, Arial, Sans-serif;-moz-user-select:none;-webkit-user-select:none;user-select:none;color:#444;line-height:16px;}
  .tvu span {margin-bottom:5px;padding: 0 0 0 18px;cursor:pointer;display:inline-block;height:16px;vertical-align:middle;background: url('') no-repeat;background-position:0px 0px;}
  .tvu span:hover { text-decoration: underline;}
  @media screen and (-webkit-min-device-pixel-ratio:0){
    .tvu{ -webkit-animation: webkit-adjacent-element-selector-bugfix infinite 1s; }
    @-webkit-keyframes webkit-adjacent-element-selector-bugfix {from {padding:0;} to {padding:0;} }
  }
  #uploader {position:absolute;top:0;right:0;left:0;height:28px;line-height:24px;padding-left:10px;background-color:#444;color:#EEE;}
  #tree {position:absolute;top:28px;bottom:0;left:0;width:160px;padding:8px;}
  #editor, #preview {position:absolute;top:28px;right:0;bottom:0;left:160px;border-left:1px solid #EEE;}
  #preview {background-color:#EEE;padding:5px;}
</style>
<script>
function createFileUploader(element, tree, editor){
  var xmlHttp;
  var input = document.createElement("input");
  input.type = "file";
  input.multiple = false;
  input.name = "data";
  document.getElementById(element).appendChild(input);
  var path = document.createElement("input");
  path.id = "upload-path";
  path.type = "text";
  path.name = "path";
  path.defaultValue = "/";
  document.getElementById(element).appendChild(path);
  var button = document.createElement("button");
  button.innerHTML = 'Upload';
  document.getElementById(element).appendChild(button);
  var mkfile = document.createElement("button");
  mkfile.innerHTML = 'MkDir';
  document.getElementById(element).appendChild(mkfile);

  function httpPostProcessRequest(){
    if (xmlHttp.readyState == 4){
      if(xmlHttp.status != 200) alert("ERROR["+xmlHttp.status+"]: "+xmlHttp.responseText);
      else {
        tree.refreshPath(path.value);
      }
    }
  }
  function createPath(p){
    xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = httpPostProcessRequest;
    var formData = new FormData();
    formData.append("path", p);
    xmlHttp.open("PUT", "/edit");
    xmlHttp.send(formData);
  }
  
  mkfile.onclick = function(e){
    if(path.value.indexOf(".") === -1) return;
    createPath(path.value);
    editor.loadUrl(path.value);
  };
  button.onclick = function(e){
    if(input.files.length === 0){
      return;
    }
    xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = httpPostProcessRequest;
    var formData = new FormData();
    formData.append("data", input.files[0], path.value);
    xmlHttp.open("POST", "/edit");
    xmlHttp.send(formData);
  };
  input.onchange = function(e){
    if(input.files.length === 0) return;
    var filename = input.files[0].name;
    var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
    var name = /(.*)\.[^.]+$/.exec(filename)[1];
    if(typeof name !== undefined){
      filename = name;
    }
    if(typeof ext !== undefined){
      if(ext === "html") ext = "htm";
      else if(ext === "jpeg") ext = "jpg";
      filename = filename + "." + ext;
    }
    if(path.value === "/" || path.value.lastIndexOf("/") === 0){
      path.value = "/"+filename;
    } else {
      path.value = path.value.substring(0, path.value.lastIndexOf("/")+1)+filename;
    }
  };
}

function createTree(element, editor){
  var preview = document.getElementById("preview");
  var treeRoot = document.createElement("div");
  treeRoot.className = "tvu";
  document.getElementById(element).appendChild(treeRoot);

  function loadDownload(path){
    document.getElementById('download-frame').src = path+"?download=true";
  }

  function loadPreview(path){
    document.getElementById("editor").style.display = "none";
    preview.style.display = "block";
    preview.innerHTML = '<img src="'+path+'?_cb='+Date.now()+'" style="max-width:100%; max-height:100%; margin:auto; display:block;" />';
  }

  function fillFileMenu(el, path){
    var list = document.createElement("ul");
    el.appendChild(list);
    var action = document.createElement("li");
    list.appendChild(action);
    if(isTextFile(path)){
      action.innerHTML = "<span>Edit</span>";
      action.onclick = function(e){
        editor.loadUrl(path);
        if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
      };
    } else if(isImageFile(path)){
      action.innerHTML = "<span>Preview</span>";
      action.onclick = function(e){
        loadPreview(path);
        if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
      };
    }
    var download = document.createElement("li");
    list.appendChild(download);
    download.innerHTML = "<span>Download</span>";
    download.onclick = function(e){
      loadDownload(path);
      if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
    };
    var delFile = document.createElement("li");
    list.appendChild(delFile);
    delFile.innerHTML = "<span>Delete</span>";
    delFile.onclick = function(e){
      httpDelete(path);
      if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
    };
  }

  function showContextMenu(e, path, isfile){
    var divContext = document.createElement("div");
    var scrollTop = document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop;
    var scrollLeft = document.body.scrollLeft ? document.body.scrollLeft : document.documentElement.scrollLeft;
    var left = event.clientX + scrollLeft;
    var top = event.clientY + scrollTop;
    divContext.className = 'cm';
    divContext.style.display = 'block';
    divContext.style.left = left + 'px';
    divContext.style.top = top + 'px';
    fillFileMenu(divContext, path);
    document.body.appendChild(divContext);
    var width = divContext.offsetWidth;
    var height = divContext.offsetHeight;
    divContext.onmouseout = function(e){
      if(e.clientX < left || e.clientX > (left + width) || e.clientY < top || e.clientY > (top + height)){
        if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(divContext);
      }
    };
  }

  function createTreeLeaf(path, name, size){
    var leaf = document.createElement("li");
    leaf.id = (((path == "/")?"":path)+"/"+name);
    var label = document.createElement("span");
    label.innerText = name;
    leaf.appendChild(label);
    leaf.onclick = function(e){
      if(isTextFile(leaf.id.toLowerCase())){
        editor.loadUrl(leaf.id);
      } else if(isImageFile(leaf.id.toLowerCase())){
        loadPreview(leaf.id);
      }
    };
    leaf.oncontextmenu = function(e){
      e.preventDefault();
      e.stopPropagation();
      showContextMenu(e, leaf.id, true);
    };
    return leaf;
  }

  function addList(parent, path, items){
    var list = document.createElement("ul");
    parent.appendChild(list);
    var ll = items.length;
    for(var i = 0; i < ll; i++){
      if(items[i].type === "file")
        list.appendChild(createTreeLeaf(path, items[i].name, items[i].size));
    }

  }

  function isTextFile(path){
    var ext = /(?:\.([^.]+))?$/.exec(path)[1];
    if(typeof ext !== undefined){
      switch(ext){
        case "txt":
        case "htm":
        case "js":
        case "c":
        case "cpp":
        case "css":
        case "xml":
        case "ini":
        case "json":
          return true;
      }
    }
    return false;
  }

  function isImageFile(path){
    var ext = /(?:\.([^.]+))?$/.exec(path)[1];
    if(typeof ext !== undefined){
      switch(ext){
        case "png":
        case "jpg":
        case "gif":
          return true;
      }
    }
    return false;
  }

  this.refreshPath = function(path){
    treeRoot.removeChild(treeRoot.childNodes[0]);
    httpGet(treeRoot, "/");
  };

  function delCb(path){
    return function(){
      if (xmlHttp.readyState == 4){
        if(xmlHttp.status != 200){
          alert("ERROR["+xmlHttp.status+"]: "+xmlHttp.responseText);
        } else {
          treeRoot.removeChild(treeRoot.childNodes[0]);
          httpGet(treeRoot, "/");
        }
      }
    }
  }

  function httpDelete(filename){
    xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = delCb(filename);
    var formData = new FormData();
    formData.append("path", filename);
    xmlHttp.open("DELETE", "/edit");
    xmlHttp.send(formData);
  }

  function getCb(parent, path){
    return function(){
      if (xmlHttp.readyState == 4){
        if(xmlHttp.status == 200) addList(parent, path, JSON.parse(xmlHttp.responseText));
      }
    }
  }

  function httpGet(parent, path){
    xmlHttp = new XMLHttpRequest(parent, path);
    xmlHttp.onreadystatechange = getCb(parent, path);
    xmlHttp.open("GET", "/list?dir="+path, true);
    xmlHttp.send(null);
  }

  httpGet(treeRoot, "/");
  return this;
}

function createEditor(element, file, lang, theme, type){
  function getLangFromFilename(filename){
    var lang = "plain";
    var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
    if(typeof ext !== undefined){
      switch(ext){
        case "txt": lang = "plain"; break;
        case "htm": lang = "html"; break;
        case "js": lang = "javascript"; break;
        case "c": lang = "c_cpp"; break;
        case "cpp": lang = "c_cpp"; break;
        case "css":
        case "scss":
        case "php":
        case "html":
        case "json":
        case "xml":
          lang = ext;
      }
    }
    return lang;
  }

  if(typeof file === "undefined") file = "/index.htm";

  if(typeof lang === "undefined"){
    lang = getLangFromFilename(file);
  }

  if(typeof theme === "undefined") theme = "monokai";

  if(typeof type === "undefined"){
    type = "text/"+lang;
    if(lang === "c_cpp") type = "text/plain";
  }

  var xmlHttp = null;
  var editor = ace.edit(element);
  function httpPostProcessRequest(){
    if (xmlHttp.readyState == 4){
      if(xmlHttp.status != 200) alert("ERROR["+xmlHttp.status+"]: "+xmlHttp.responseText);
    }
  }
  function httpPost(filename, data, type){
    xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = httpPostProcessRequest;
    var formData = new FormData();
    formData.append("data", new Blob([data], { type: type }), filename);
    xmlHttp.open("POST", "/edit");
    xmlHttp.send(formData);
  }
  function httpGetProcessRequest(){
    if (xmlHttp.readyState == 4){
      document.getElementById("preview").style.display = "none";
      document.getElementById("editor").style.display = "block";
      if(xmlHttp.status == 200) editor.setValue(xmlHttp.responseText);
      else editor.setValue("");
      editor.clearSelection();
    }
  }
  function httpGet(theUrl){
      xmlHttp = new XMLHttpRequest();
      xmlHttp.onreadystatechange = httpGetProcessRequest;
      xmlHttp.open("GET", theUrl, true);
      xmlHttp.send(null);
  }

  if(lang !== "plain") editor.getSession().setMode("ace/mode/"+lang);
  editor.setTheme("ace/theme/"+theme);
  editor.$blockScrolling = Infinity;
  editor.getSession().setUseSoftTabs(true);
  editor.getSession().setTabSize(2);
  editor.setHighlightActiveLine(true);
  editor.setShowPrintMargin(false);
  editor.commands.addCommand({
      name: 'saveCommand',
      bindKey: {win: 'Ctrl-S',  mac: 'Command-S'},
      exec: function(editor) {
        httpPost(file, editor.getValue()+"", type);
      },
      readOnly: false
  });
  editor.commands.addCommand({
      name: 'undoCommand',
      bindKey: {win: 'Ctrl-Z',  mac: 'Command-Z'},
      exec: function(editor) {
        editor.getSession().getUndoManager().undo(false);
      },
      readOnly: false
  });
  editor.commands.addCommand({
      name: 'redoCommand',
      bindKey: {win: 'Ctrl-Shift-Z',  mac: 'Command-Shift-Z'},
      exec: function(editor) {
        editor.getSession().getUndoManager().redo(false);
      },
      readOnly: false
  });
  httpGet(file);
  editor.loadUrl = function(filename){
    file = filename;
    lang = getLangFromFilename(file);
    type = "text/"+lang;
    if(lang !== "plain") editor.getSession().setMode("ace/mode/"+lang);
    httpGet(file);
  };
  return editor;
}
function onBodyLoad(){
  var vars = {};
  var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { vars[key] = value; });
  var editor = createEditor("editor", vars.file, vars.lang, vars.theme);
  var tree = createTree("tree", editor);
  createFileUploader("uploader", tree, editor);
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ace.js" type="text/javascript" charset="utf-8"></script>
</head>
<body onload="onBodyLoad();">
  <div id="uploader"></div>
  <div id="tree"></div>
  <div id="editor"></div>
  <div id="preview" style="display:none;"></div>
  <iframe id=download-frame style='display:none;'></iframe>
</body>
</html>
